﻿using System;
using System.Collections.Generic;
using System.Text;
using EnvDTE;

namespace CommentHero
{
    #region 置換文字列構造体
    /// <summary>
    /// 置換文字列構造体
    /// </summary>
    /// <remarks>
    /// コメント文に含まれている置換対象の文字列です。
    /// </remarks>
    internal struct ReplaceString
    {
        internal const string XML_COMMENT = @"\\xmlcomment\\";
        internal const string NAMESPACE = @"\\namespace\\";
        internal const string CLASS = @"\\class\\";
        internal const string METHOD = @"\\method\\";
        internal const string PROPERTY = @"\\property\\";
        internal const string CONSTRUCTOR = @"\\constructor\\";
        internal const string INTERFACE = @"\\interface\\";
        internal const string DATE = @"\\date\\";
        internal const string AUTHER = @"\\auther\\";
    }
    #endregion


    /// <summary>
    /// コメント処理クラス
    /// </summary>
    /// <remarks>
    /// コメント処理を行うクラスです。
    /// </remarks>
    internal class CommentLogic
    {
        #region フィールド
        private readonly TextDocument _textDocument;
        private readonly Comments _comments;
        private readonly string _xmlCommentSymbol;

        private CodeType _codeType = CodeType.None; // コード種別

        private string _nameSpace = string.Empty; // 置換名前空間
        private string _className = string.Empty; // 置換クラス名
        private string _methodName = string.Empty; // 置換メソッド名
        private string _propertyName = string.Empty; // 置換プロパティ名
        private List<string> _methodParameter = new List<string>(); // XMLコメント用メソッドパラメータ
        private string _methodType = string.Empty; // XMLコメント用メソッド型

        private const string CODEMODEL_CS = "CSharp";
        private const string CODEMODEL_VB = "Basic";
        private const string CRLF = "\r\n";

        private const string TEST_ATTRIBUTE_FULLNAME = "NUnit.Framework.TestAttribute";
        #endregion

        #region 列挙値
        /// <summary>
        /// コード種別
        /// </summary>
        /// <remarks>
        /// 処理されるコードの種類です。
        /// </remarks>
        private enum CodeType
        {
            /// <summary>定義なし(ブロックコメント等)</summary>
            None,
            /// <summary>クラス</summary>
            Class,
            /// <summary>メソッド</summary>
            Method,
            /// <summary>プロパティ</summary>
            Property,
            /// <summary>コンストラクタ</summary>
            Constructor,
            /// <summary>インターフェース</summary>
            Interface
        }
        #endregion

        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <remarks>
        /// 初期処理を行います。
        /// </remarks>
        /// <param name="document">TextDocumentオブジェクト</param>
        public CommentLogic(TextDocument document)
        {
            this._textDocument = document;

            // 言語に合わせたコメント定義オブジェクトを取得
            switch (this._textDocument.Language)
            {
                case CODEMODEL_CS:
                    // C#の場合
                    this._xmlCommentSymbol = "/// ";
                    this._comments = ConfigReader.Instance.CSComments;
                    break;
                case CODEMODEL_VB:
                    // VBの場合
                    this._xmlCommentSymbol = "''' ";
                    this._comments = ConfigReader.Instance.VBComments;
                    break;
                default:
                    return;
            }
        }
        #endregion

        #region クラスコメント追加
        /// <summary>
        /// クラスコメント追加
        /// </summary>
        /// <remarks>
        /// CommentAddin.Connect.AddClassCommentの処理を行います。
        /// </remarks>
        internal void AddClassComment()
        {
            TextSelection textSelection = this._textDocument.Selection;

            CodeElement element = GetCodeElement(textSelection, CodeType.Class);
            if (element != null)
            {
                HeaderComment headerComment = null;
                switch (element.Kind)
                {
                    case vsCMElement.vsCMElementClass:
                        this._codeType = CodeType.Class;
                        headerComment = this._comments.ClassHeaderComment;
                        break;

                    case vsCMElement.vsCMElementInterface:
                        this._codeType = CodeType.Interface;
                        headerComment = this._comments.InterfaceHeaderComment;
                        break;
                }
                textSelection.MoveToPoint(element.StartPoint, false);
                WriteComment(textSelection, headerComment);
            }
        }
        #endregion

        #region プロシージャ(メソッド/プロパティ)コメント追加
        /// <summary>
        /// プロシージャコメント追加
        /// </summary>
        /// <summary>
        /// CommentAddin.Connect.AddProcedureCommentの処理を行います。
        /// </summary>
        internal void AddProcedureComment()
        {
            TextSelection textSelection = this._textDocument.Selection;

            CodeElement element = GetCodeElement(textSelection, CodeType.Method);
            if (element != null)
            {
                // CodeElementが取得できたらメソッドの処理を行う
                CodeFunction functionElement = (CodeFunction)element;

                HeaderComment headerComment = null;

                bool isNUnitTestMethod = false;
                foreach (CodeAttribute ca in functionElement.Attributes)
                {
                    if (ca.FullName == TEST_ATTRIBUTE_FULLNAME)
                    {
                        isNUnitTestMethod = true;
                        break;
                    }
                }

                if (isNUnitTestMethod)
                {
                    //TODO:NUnitテストメソッドだった場合はテストケース用コメントにしよう
                    headerComment = this._comments.TestCaseHeaderComment;
                }
                else if (functionElement.FunctionKind == vsCMFunction.vsCMFunctionConstructor)
                {
                    // コンストラクタ
                    headerComment = this._comments.ConstructorHeaderComment;
                }
                else
                {
                    // コンストラクタ以外のメソッド                    
                    headerComment = this._comments.MethodHeaderComment;
                }
                this._codeType = CodeType.Method;
                // 出力位置合わせ
                textSelection.MoveToPoint(element.StartPoint, false);
                // 出力
                WriteComment(textSelection, headerComment);

                return;
            }

            element = GetCodeElement(textSelection, CodeType.Property);
            if (element != null)
            {
                // CodeElementが取得できたらプロパティ処理を行う
                this._codeType = CodeType.Property;
                // 出力位置合わせ
                textSelection.MoveToPoint(element.StartPoint, false);
                // 出力
                WriteComment(textSelection, this._comments.PropertyHeaderComment);
            }
        }
        #endregion

        #region ブロックコメント追加
        /// <summary>
        /// ブロックコメント追加
        /// </summary>
        /// <remarks>
        /// CommentAddin.Connect.AddBlockCommentの処理を行います。
        /// </remarks>
        internal void AddBlockComment()
        {
            TextSelection ts = this._textDocument.Selection;

            GetCodeElement(ts, CodeType.None);

            // 出力位置を先頭に
            ts.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText, false);

            // 出力
            WriteComment(ts, this._comments.BlockComment);

            return;
        }
        #endregion

        #region カスタムコメント追加
        /// <summary>
        /// カスタムコメント追加
        /// </summary>
        /// <remarks>
        /// CommentAddin.Connect.AddCustomComment1～5の処理を行います。
        /// </remarks>
        /// <param name="customCommentNumber">カスタムコメント番号(1～5)</param>
        internal void AddCustomComment(int customCommentNumber)
        {
            TextSelection ts = this._textDocument.Selection;

            GetCodeElement(ts, CodeType.None);

            // 出力位置を先頭に
            ts.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText, false);

            // 出力
            WriteComment(ts, this._comments.GetCustomComment(customCommentNumber));

            return;
        }
        #endregion

        #region CodeElement取得
        /// <summary>
        /// CodeElement取得処理
        /// </summary>
        /// <remarks>
        /// CodeElementを取得、解析後にCodeTypeに合わせて返却します。
        /// </remarks>
        /// <param name="textSelection">TextSelectionオブジェクト</param>
        /// <param name="returnType">返却されるCodeElementの種類</param>
        /// <returns>CodeElement(returnType = Noneの場合はnullを返却)</returns>
        private CodeElement GetCodeElement(TextSelection textSelection, CodeType returnType)
        {
            // クラス
            CodeElement classElement;
            classElement = textSelection.ActivePoint.get_CodeElement(vsCMElement.vsCMElementClass);
            if (classElement != null)
            {
                // クラス名取得
                this._className = classElement.Name;
            }
            else
            {
                classElement = textSelection.ActivePoint.get_CodeElement(vsCMElement.vsCMElementInterface);
                if (classElement != null)
                {
                    this._className = classElement.Name;
                }
            }

            // メソッド
            CodeElement methodElement;
            methodElement = textSelection.ActivePoint.get_CodeElement(vsCMElement.vsCMElementFunction);
            if (methodElement != null)
            {
                CodeFunction element = (CodeFunction)methodElement;

                // メソッド名取得
                this._methodName = element.Name;

                if (element.Type.TypeKind != vsCMTypeRef.vsCMTypeRefVoid)
                {
                    // void(C#), System.Void(VB)以外ならメソッド型を取得
                    this._methodType = element.Type.AsString;
                }

                int parameterCount = element.Parameters.Count;
                for (int i = 0; i < parameterCount; i++)
                {
                    // 引数名を取得
                    this._methodParameter.Add(element.Parameters.Item(i + 1).Name);
                }
            }

            // プロパティ
            CodeElement propertyElement = textSelection.ActivePoint.get_CodeElement(vsCMElement.vsCMElementProperty);
            if (propertyElement != null)
            {
                // プロパティ名取得
                this._propertyName = propertyElement.Name;
            }

            // CodeTypeに合わせた返却を行う
            switch (returnType)
            {
                case CodeType.Class:
                    return classElement;
                case CodeType.Method:
                    return methodElement;
                case CodeType.Property:
                    return propertyElement;
                case CodeType.None:
                default:
                    return null;
            }
        }
        #endregion

        #region コメント出力
        /// <summary>
        /// コメント用出力処理(ヘッダコメント用)
        /// </summary>
        /// <param name="textSelection">TextSelectionオブジェクト</param>
        /// <param name="headerComment">ヘッダコメントオブジェクト</param>
        private void WriteComment(TextSelection textSelection, HeaderComment headerComment)
        {
            int indent = textSelection.CurrentColumn - 1;
            List<string> outputText = new List<string>();

            foreach (string addText in headerComment.Comment)
            {
                if (addText.Contains(ReplaceString.XML_COMMENT))
                {
                    // \\xmlcomment\\が含まれている行はXMLコメントに置換
                    CreateXMLComment(outputText, headerComment.XmlSummary, headerComment.XmlRemarks);
                }
                else
                {
                    outputText.Add(addText);
                }
            }

            WriteComment(textSelection, outputText);
        }

        /// <summary>
        /// コメント出力処理
        /// </summary>
        /// <remarks>
        /// コメントを出力します。
        /// </remarks>
        /// <param name="textSelection">TextSelectionオブジェクト</param>
        /// <param name="textList">コメントが格納された文字列型コレクション</param>
        private void WriteComment(TextSelection textSelection, List<string> textList)
        {
            string text = string.Empty;

            // ドキュメント左端からの出力位置
            string newLine = CRLF + new string(' ', textSelection.CurrentColumn - 1);

            foreach (string addText in textList)
            {
                string replacedText = ReplaceText(addText);
                text += replacedText + newLine;

            }

            // 出力
            textSelection.Insert(text, (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);
            textSelection.LineUp(false, 1);
            textSelection.EndOfLine(false);
        }

        /// <summary>
        /// XMLコメント生成
        /// </summary>
        /// <remarks>
        /// \\xmlcomment\\置換用のXMLコメント文字列を生成します。
        /// </remarks>
        /// <param name="outputText">追加用文字列リスト</param>
        /// <param name="summary">summaryタグコメント文字列</param>
        /// <param name="remarks">remarksタグコメント文字列</param>
        private void CreateXMLComment(List<string> outputText, List<string> summary, List<string> remarks)
        {
            // <summary>タグ生成
            outputText.Add(this._xmlCommentSymbol + "<summary>");
            if (summary.Count == 0)
            {
                outputText.Add(this._xmlCommentSymbol);
            }
            else
            {
                foreach (string str in summary)
                {
                    outputText.Add(this._xmlCommentSymbol + str);
                }
            }
            outputText.Add(this._xmlCommentSymbol + "</summary>");

            // <remarks>タグ生成
            outputText.Add(this._xmlCommentSymbol + "<remarks>");
            if (remarks.Count == 0)
            {
                outputText.Add(this._xmlCommentSymbol);
            }
            else
            {
                foreach (string str in remarks)
                {
                    outputText.Add(this._xmlCommentSymbol + str);
                }
            }
            outputText.Add(this._xmlCommentSymbol + "</remarks>");

            switch (this._codeType)
            {
                case CodeType.Class:
                case CodeType.Interface:
                    // クラスの場合
                    break;

                case CodeType.Method:
                    // メソッドの場合
                    if (this._methodParameter.Count > 0)
                    {
                        // 引数毎に<param>タグ生成
                        foreach (string argumentName in this._methodParameter)
                        {
                            string paramText = this._xmlCommentSymbol
                                + string.Format("<param name=\"{0}\"></param>", argumentName);
                            outputText.Add(paramText);
                        }
                    }

                    if (!string.IsNullOrEmpty(this._methodType))
                    {
                        // 返却値があれば<returns>タグ生成
                        outputText.Add(this._xmlCommentSymbol + "<returns></returns>");
                    }

                    break;

                case CodeType.Property:
                    // プロパティの場合
                    // <value>タグ生成
                    outputText.Add(this._xmlCommentSymbol + "<value></value>");
                    break;

                case CodeType.None:
                    // その他コメントの場合
                    break;

                default:
                    break;
            }
        }

        /// <summary>
        /// 文字列置換
        /// </summary>
        /// <remarks>
        /// コメントに含まれる置換文字列を処理します。
        /// </remarks>
        /// <param name="text">置換対象文字列</param>
        /// <returns>置換後文字列</returns>
        private string ReplaceText(string text)
        {
            //text = text.Replace(ReplaceString.NAMESPACE, "namespace");
            text = text.Replace(ReplaceString.CLASS, this._className);
            text = text.Replace(ReplaceString.METHOD, this._methodName);
            text = text.Replace(ReplaceString.PROPERTY, this._propertyName);
            text = text.Replace(ReplaceString.CONSTRUCTOR, this._methodName);
            text = text.Replace(ReplaceString.INTERFACE, this._className);
            text = text.Replace(ReplaceString.DATE, DateTime.Now.ToString(ConfigReader.Instance.DateFormat));
            text = text.Replace(ReplaceString.AUTHER, ConfigReader.Instance.Author);

            return text;
        }
        #endregion

        /// <summary>
        /// Region追加(実装中)
        /// </summary>
        internal void AddRegion()
        {
            //TODO:リージョンラベルを自動で記入したい

            TextSelection textSelection = this._textDocument.Selection;
            EditPoint startPoint = textSelection.TopPoint.CreateEditPoint();
            EditPoint endPoint = textSelection.BottomPoint.CreateEditPoint();

            startPoint.StartOfLine();
            startPoint.Insert("#region InsertRegion" + CRLF);

            endPoint.EndOfLine();
            endPoint.Insert(CRLF + "#endregion");

            // InsertRegionを選択状態に
            textSelection.MoveTo(startPoint.Line - 1, startPoint.DisplayColumn, false);
            textSelection.EndOfLine(false);
            textSelection.WordLeft(true, 1);
        }
    }
}
